﻿using iTool.Cloud.Center;
using iTool.Cloud.Center.Model;
using iTool.Clustering.Center.ServiceProvider;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Orleans;
using Orleans.Configuration;
using Orleans.Runtime;
using Orleans.Transactions;
using Orleans.Transactions.Abstractions;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace iTool.Clustering.Center.Membership
{

    public class iCenterTransactionalStateStorageFactory : ITransactionalStateStorageFactory
    {
        private readonly string name;
        private readonly ClusterOptions clusterOptions;
        private readonly Newtonsoft.Json.JsonSerializerSettings jsonSettings;

        public static ITransactionalStateStorageFactory Create(IServiceProvider services, string name)
        {
            return ActivatorUtilities.CreateInstance<iCenterTransactionalStateStorageFactory>(services, name);
        }

        public iCenterTransactionalStateStorageFactory(string name,
            IOptions<ClusterOptions> clusterOptions,
            ITypeResolver typeResolver,
            IGrainFactory grainFactory)
        {
            this.name = name;
            this.jsonSettings = TransactionalStateFactory.GetJsonSerializerSettings(
                typeResolver,
                grainFactory);
            this.clusterOptions = clusterOptions.Value;
        }

        public ITransactionalStateStorage<TState> Create<TState>(string stateName, IGrainActivationContext context) where TState : class, new()
        {
            return ActivatorUtilities.CreateInstance<iCenterTransactionalStateStorageProvider<TState>>(context.ActivationServices, jsonSettings, stateName);
        }
    }

    public class iCenterTransactionalStateStorageProvider<TState> : ITransactionalStateStorage<TState>
        where TState : class, new()
    {
        private readonly string stateName;
        private readonly IGrainActivationContext context;
        private readonly JsonSerializerSettings jsonSettings;

        private IClusterTransactionalService grain;
        private ClusterOptions clusterOptions;

        private Cloud.Center.Model.TransactionalStateRecord<TState> State { get; set; } = new Cloud.Center.Model.TransactionalStateRecord<TState>();


        public iCenterTransactionalStateStorageProvider(
            iCenterClusterClient client,
            IOptions<ClusteringStaticOptions> options,
            IOptions<ClusterOptions> clusterOptions,
            IGrainActivationContext context,
            JsonSerializerSettings jsonSettings,
            string stateName
            )
        {
            this.stateName = stateName;
            this.context = context;
            this.jsonSettings = jsonSettings;
            this.clusterOptions = clusterOptions.Value;
            this.grain = client.GetGrain<IClusterTransactionalService>(new Random().Next(111_111_111, 999_999_999), this.clusterOptions.ToKeyString());
        }

        public async Task<TransactionalStorageLoadResponse<TState>> Load()
        {
            string grainKey = this.stateName + this.context.GrainInstance.GrainReference.ToShortKeyString();
            string body = await this.grain.Load(typeof(TState).FullName, grainKey);

            if (!string.IsNullOrWhiteSpace(body))
            {
                this.State = JsonConvert.DeserializeObject<Cloud.Center.Model.TransactionalStateRecord<TState>>(body, this.jsonSettings) ?? new Cloud.Center.Model.TransactionalStateRecord<TState>();
            }

            return this.State.ETag == string.Empty ?
                new TransactionalStorageLoadResponse<TState>() :
                new TransactionalStorageLoadResponse<TState>(
                    this.State.ETag,
                    this.State.CommittedState,
                    this.State.CommittedSequenceId,
                    this.State.Metadata,
                    this.State.PendingStates);
        }

        public async Task<string> Store(string expectedETag, TransactionalStateMetaData metadata, List<PendingTransactionState<TState>> statesToPrepare, long? commitUpTo, long? abortAfter)
        {
            expectedETag = expectedETag ?? string.Empty;
            // check update eTag
            if (this.State.ETag != expectedETag)
            {
                throw new ArgumentException(nameof(expectedETag), "Etag does not match");
            }

            // abort
            if (abortAfter.HasValue && this.State.PendingStates.Count > 0)
            {
                var postion = this.State.PendingStates.FindIndex(t => t.SequenceId > abortAfter.Value);
                if (postion != -1)
                {
                    this.State.PendingStates.RemoveRange(postion, this.State.PendingStates.Count - postion);
                }
            }

            // prepare
            if (statesToPrepare?.Count > 0)
            {
                foreach (var prepare in statesToPrepare)
                {
                    var postion = this.State.PendingStates.FindIndex(t => t.SequenceId >= prepare.SequenceId);
                    if (postion == -1)
                    {
                        this.State.PendingStates.Add(prepare); //append
                    }
                    else if (this.State.PendingStates[postion].SequenceId == prepare.SequenceId)
                    {
                        this.State.PendingStates[postion] = prepare;  //replace
                    }
                    else
                    {
                        this.State.PendingStates.Insert(postion, prepare); //insert
                    }
                }
            }

            // commit
            if (commitUpTo.HasValue && commitUpTo.Value > this.State.CommittedSequenceId)
            {
                var postion = this.State.PendingStates.FindIndex(t => t.SequenceId == commitUpTo.Value);
                if (postion != -1)
                {
                    var committedState = this.State.PendingStates[postion];
                    this.State.CommittedSequenceId = committedState.SequenceId;
                    this.State.CommittedState = committedState.State;
                    this.State.PendingStates.RemoveRange(0, postion + 1);
                }
                else
                {
                    throw new InvalidOperationException($"Transactional state corrupted. Missing prepare record (SequenceId={commitUpTo.Value}) for committed transaction.");
                }
            }

            string grainKey = this.stateName + this.context.GrainInstance.GrainReference.ToShortKeyString();
            this.State.ETag = Guid.NewGuid().ToString();

            await this.grain.Store(typeof(TState).FullName, grainKey, JsonConvert.SerializeObject(this.State, this.jsonSettings));

            return this.State.ETag;

        }
    }
}
